#include <arpa/inet.h>
#include <time.h>
#include <math.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <math.h>
#include <fcntl.h>
#include <ctype.h>
#include <elf.h>

static char *file_start;

#pragma pack(1)

typedef signed char s8;
typedef signed short s16;
typedef signed int s32;
typedef signed long long s64;
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;
typedef unsigned long long u64;


#if defined(__BIG_ENDIAN__) || defined(_BIG_ENDIAN)

#define le32_to_cpu(x) (\
	(((x)>>24)&0xff)\
	|\
	(((x)>>8)&0xff00)\
	|\
	(((x)<<8)&0xff0000)\
	|\
	(((x)<<24)&0xff000000)\
)

#define le16_to_cpu(x) ( (((x)>>8)&0xff) | (((x)<<8)&0xff00) )

#else

#define le32_to_cpu(x) (x)
#define le16_to_cpu(x) (x)

#endif

#define cpu_to_le32(x) le32_to_cpu(x)
#define cpu_to_le16(x) le16_to_cpu(x)

//////////////////

static void hexdump(char *prefix, char *data, int size){
  if(size > 640)
  	size = 640;
  while(size){
    int amt = (size>16) ? 16 : size;
    char samp[20];
    char hex[80];
    samp[amt]='\0';
    hex[0] = '\0';
    memcpy(samp,data,amt);
    int i = 0;
    char *hp = hex;
    while(i<amt){
      snprintf(hp,42,"%02x ",(unsigned char)samp[i]);
      hp += 3;
      if((i&3)==3){
        hp[0]=' ';
        hp[1]='\0';
        hp++;
      }
      if(!isprint(samp[i]))
        samp[i]='.';
      i++;
    }
    fprintf(stdout,"%s%-52.52s<%s>\n",prefix,hex,samp);
    data += amt;
    size -= amt;
  }
}

/////////////////////////////////////

static unsigned crctable[256];

#define POLYNOMIAL 0x04C11DB7

static void init_crc(void){
	unsigned dividend = 0;
	do{
		unsigned remainder = dividend << 24;
		unsigned bit = 0;
		do{
			if(remainder & 0x80000000){
				remainder <<= 1;
				remainder ^= POLYNOMIAL;
			}else{
				remainder <<= 1;
			}
		}while(++bit<8);
		crctable[dividend++] = remainder;
	}while(dividend<256);
}

// for Marvell, pass 0 as the initial remainder
static unsigned do_crc(unsigned remainder, const unsigned char *p, unsigned n){
	unsigned i = 0;
	while(i < n){
		unsigned char data = *p ^ (remainder >> 24);
		remainder = crctable[data] ^ (remainder << 8);
		p++;
		i++;
	}
	return remainder;
}

///////////////////////////////////////

#define need(size,msg)({        \
	if(mapsize<(int)(size)){   \
		/*fprintf(stdout,"not enough bytes, have %d of %d for %s\n",(int)mapsize,(int)(size),msg);*/     \
		return orig;     \
	}                    \
})

////////////////////////////////////

/** fwheader */
struct fwheader {
	u32 dnldcmd;    // 1, except 4 for the 0-sized block at the end
	u32 baseaddr;   // within 64 k following 0 or 0xc0000000
	u32 datalength; // The driver accepts 0 to 600. We see 512, plus short blocks at the end of a segment/section.
	u32 CRC;
};
// for sending to the chip, an extra u32 seq num goes here (before data)

#define FW_HAS_DATA_TO_RECV		0x00000001
#define FW_HAS_LAST_BLOCK		0x00000004

///////////////////////////////////////

static char *spew(char *map, ssize_t mapsize){
	char *orig = map;
	for(;;){
		if(!mapsize){
			fprintf(stdout,"ran out at file offset %zd 0x%08zx\n",map-file_start,map-file_start);
			return orig;
		}
		if(mapsize<0){
			fprintf(stdout,"INTERNAL ERROR NEGATIVE REMAINDER %zd 0x%08zx\n",map-file_start,map-file_start);
			return orig;
		}

		struct fwheader fwheader;
		need(sizeof fwheader, "fwheader");
		memcpy(&fwheader,map,sizeof fwheader);
		if(map==orig){
			if(le32_to_cpu(fwheader.dnldcmd)!=FW_HAS_DATA_TO_RECV || le32_to_cpu(fwheader.datalength)>600 || le32_to_cpu(fwheader.datalength)<42 || le32_to_cpu(fwheader.baseaddr)>0xc0027fff)
			return orig;
		}
		map += sizeof fwheader;
		mapsize -= sizeof fwheader;
		fprintf(stdout,
			"%08x  cmd:%08x addr:%08x len:%08x crc:%08x\n",
			(unsigned)(orig-file_start),
			le32_to_cpu(fwheader.dnldcmd),
			le32_to_cpu(fwheader.baseaddr),
			le32_to_cpu(fwheader.datalength),
			le32_to_cpu(fwheader.CRC)
		);
		if(do_crc(0,(unsigned char*)&fwheader,16)){
			fprintf(stdout,"bad header CRC\n");
			return orig;
		}
		if(le32_to_cpu(fwheader.dnldcmd)==FW_HAS_LAST_BLOCK && fwheader.datalength==0){
			fprintf(stdout,"Got one! Size 0x%08x.\n", (unsigned)(map-orig));
			return map;
		}
		if(le32_to_cpu(fwheader.dnldcmd)!=FW_HAS_DATA_TO_RECV || le32_to_cpu(fwheader.datalength) > 600){
			fprintf(stdout,"oh crap\n");
			hexdump("x ",orig,mapsize+sizeof fwheader);
			return orig;
		}
		need(le32_to_cpu(fwheader.datalength), "data");
		int len = le32_to_cpu(fwheader.datalength) - 4;
		int addr = le32_to_cpu(fwheader.baseaddr) & 0x0003ffff;
		int past = addr + len;
		if(past>0x40000)
			return orig;
		if(do_crc(0,(unsigned char*)map,len+4)){
			fprintf(stdout,"bad body CRC\n");
			return orig;
		}

		switch(le32_to_cpu(fwheader.baseaddr) & 0xfffc0000u){
		case 0xc0000000:
		case 0x00000000:
		case 0x04000000:
			break;
		default:
			fprintf(stdout,"le32_to_cpu(fwheader.baseaddr) is 0x%08x\n",le32_to_cpu(fwheader.baseaddr));
			return orig;
		}

//		hexdump("data ",map,le32_to_cpu(fwheader.datalength));
		map += le32_to_cpu(fwheader.datalength);
		mapsize -= le32_to_cpu(fwheader.datalength);

//		hexdump("junk ",map,mapsize);
	}
}

static void mapit(const char *restrict const name){
  int fd = open(name, O_RDONLY);
  if(fd<3){
    perror("open");
    exit(88);
  }
  struct stat sbuf;
  fstat(fd,&sbuf);
  char *map;
  map = mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
  close(fd);
  file_start = map;

  if(!map || map==(char*)-1){
  	fprintf(stdout,"%d is a bad size or mapping failed\n",(int)sbuf.st_size);
  	exit(3);
  }
  int mapsize = sbuf.st_size;
//  fprintf(stdout,"mapped    %6d bytes\n",mapsize);
  while(mapsize >= 24){
    char *end = spew(map,mapsize);
    unsigned sz = end-map;
    if(sz >= 24){
      char path[64];
      snprintf(path,sizeof path,"%.11s-%08x.bin",name,(unsigned)(map-file_start));
      FILE *fp = fopen(path,"w");
      fwrite(map,sz,1,fp);
      fclose(fp);
    }else{
      sz = 1;
    }
    map += sz;
    mapsize -= sz;
  }
}

int main(int argc, char *argv[]){
  init_crc();
//  fprintf(stdout,"filename %s\n",argv[1]);
  mapit(argv[1]);
  return 0;
}
